Repository navigation
[bugfix] audio: stop a stale voice from stranding a speech node - #242
Merged
Merged
Conversation
## summary - Audio settings now drop a stored voice selection when the resolved model publishes no voice library. `serviceAudioSettings` runs on the composer's read path (`getNodeGenerationSettings`), so a node reopened after its model configuration changed is no longer blocked. - Before this, only a model *switch* cleared the selection. Editing the same configuration into a plain OpenAI-compatible TTS kept the voice, and `voiceIssue` reported it as belonging to another service. The composer offers the library only while that capability exists, so there was no control left to replace it and every later edit re-applied the same stale value. - Dropping the voice is the same outcome the switch path already produces, and a model that still publishes a library keeps its selection untouched. ## validation - `tests/voiceLibrary.test.ts`: 13 passed. Four cover this change — the rules level (blocked while stale, unblocked after servicing, unchanged for a model that keeps the library) and the read boundary the composer actually uses. - Removing the `serviceAudioSettings` hunk makes the read-path test fail, so the regression is pinned. - `node --test tests/*.test.ts`: 666 tests, 658 passed, 8 failed. The same 8 fail on a clean `main` checkout here because `node_modules` is not installed locally (`ERR_MODULE_NOT_FOUND: react`); the count is identical with and without this change. - `npm run check:i18n`: 2935 keys, 0 problems. No new user-visible string. Refs: #218
kunkunblueberry
requested review from
hsliuustc0106 and
princepride
as code owners
November 9, 2026 10:11
5 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A speech node could stay blocked on a voice its model can no longer use.
What happened
The composer offers the voice library only while the resolved model publishes
that capability (
NodeComposer.tsx:110readsinputRules.voiceLibrary). A node'sstored
voiceSelectionwas cleared only inside the model-switch branch(
rules.ts:431if (modelChanged)), which runs withmodelChanged = truefromthe picker and from
newNodeGenerationSettings.So after an operator edited the same model configuration into a plain
OpenAI-compatible TTS — the node keeps its
modelConfigId, so nothing counts asa switch — the stored voice survived,
voiceIssue(selection, undefined)reportedvoiceLibrary.connectionChanged, andbackendBlockerreturned "该音色属于另一服务或账户。请从当前音色库重新选择。" while there was no library left to re-select from. Every
later composer edit re-applied the same value through
serviceGenerationSettings(..., modelChanged = false), so the node stayedblocked until the user switched models.
Measured before the change, on
63100975:What changed
serviceAudioSettingsnow drops the selection when the model publishes no voicelibrary. That function is on the read path the composer actually uses —
getNodeGenerationSettings(generationSettings.ts:76) — so the drop happenswhen the node is read, not only after a switch:
voiceNameis cleared with it becauserules.ts:522requiresvoiceName === voiceSelection.voiceId, so leaving the id behind would trade oneblocker for "select a voice". This is the same outcome the model-switch branch
already produced. A model that still publishes a library is untouched, and a
connection whose
connectionIdchanged is untouched too — that case keeps itsdeliberate block because the library is still there to re-select from.
Validation
Four tests cover this change:
a model that lost its voice library cannot strand a node on the old voice—asserts the raw settings are blocked, then that servicing clears both fields
and the blocker.
a model that keeps its voice library still retains the stored selection—guards against over-clearing.
reopening a node on a model without the voice library drops the stored voiceand
reopening a node on a model that keeps the library still reads its voice— the same two cases through
getNodeGenerationSettings, which is the boundarythe composer and the run share.
Removing the
serviceAudioSettingshunk makes the read-path test fail, so theregression is pinned rather than asserted after the fact.
The 8 failures in the full run are identical on a clean
maincheckout here:node_modulesis not installed locally, so anything importingreactfails withERR_MODULE_NOT_FOUND. The set and count are the same with and without thischange.
tsc --noEmitcould not run locally for the same reason; the Releasechecks lane covers it.
Not covered
The composer was not exercised in a browser. The claim rests on
getNodeGenerationSettingsbeing the single read boundary for node settings,which the two read-path tests do exercise.